﻿#region File and License Information
/*
<File>
	<License>
		Copyright © 2009 - 2017, Daniel Vaughan. All rights reserved.
		This file is part of Calcium (http://calciumsdk.net), 
		which is released under the MIT License.
		See file /Documentation/License.txt for details.
	</License>
	<CreationDate>2010-10-29 17:45:22Z</CreationDate>
</File>
*/
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

using Outcoder.Concurrency;

/* Thanks to Glenn Block http://codepaste.net/f5zs2o, 
 * for which his idea and this code is based on. */

namespace Outcoder.Messaging
{
	public class Messenger : IMessenger
	{
		readonly IProvider<ISynchronizationContext> contextProvider;

		readonly Dictionary<Type, List<WeakReference>> subscriberLists 
			= new Dictionary<Type, List<WeakReference>>();

		readonly Dictionary<Type, List<Type>> registeredTypes 
			= new Dictionary<Type, List<Type>>();

		readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim();

		public Messenger() : this((IProvider<ISynchronizationContext>)null)
		{
			/* Intentionally left blank. */
		}

		public Messenger(IProvider<ISynchronizationContext> contextProvider)
		{
			this.contextProvider = contextProvider ?? new UIContextProvider();
		}

		public void Subscribe(object subscriber)
		{
			ArgumentValidator.AssertNotNull(subscriber, "subscriber");

			var weakReference = new WeakReference(subscriber);

			Type subscriberType = subscriber.GetType();

			lockSlim.EnterWriteLock();
			try
			{

#if NETFX_CORE
				foreach (Type interfaceType in subscriberType.GetTypeInfo().ImplementedInterfaces)
				{
					if (interfaceType.GetTypeInfo().IsGenericType
					    && interfaceType.GetGenericTypeDefinition() == typeof(IMessageSubscriber<>))
					{
#else
				foreach (Type interfaceType in subscriberType.GetInterfaces())
				{
					if (interfaceType.IsGenericType
							&& interfaceType.GetGenericTypeDefinition() 
							== typeof(IMessageSubscriber<>))
					{
#endif
						bool possibleDuplicate = false;
						List<Type> typesForThisMessage;
						if (registeredTypes.TryGetValue(interfaceType, out typesForThisMessage))
						{
							if (typesForThisMessage.Contains(subscriberType))
							{
								possibleDuplicate = true;
							}
						}
						else
						{
							typesForThisMessage = new List<Type>();
							registeredTypes[interfaceType] = typesForThisMessage;
						}

						typesForThisMessage.Add(subscriberType);

						List<WeakReference> subscribers = GetSubscribersNonLocking(interfaceType);
						if (possibleDuplicate)
						{
							/* We may want to improve search complexity here for large sets; 
							 * perhaps using a ConditionalWeakTable. */
							foreach (WeakReference reference in subscribers)
							{
								if (reference.Target == subscriber)
								{
									return;
								}
							}
						}
						subscribers.Add(weakReference);
					}
				}
			}
			finally
			{
				lockSlim.ExitWriteLock();
			}
		}

		public void Unsubscribe(object subscriber)
		{
			lockSlim.EnterWriteLock();
			try
			{
				foreach (KeyValuePair<Type, List<WeakReference>> pair in subscriberLists.ToList())
				{
					List<WeakReference> list = pair.Value;

					list?.RemoveAll(x => x.Target == subscriber);
				}
			}
			finally
			{
				lockSlim.ExitWriteLock();
			}
		}

		public PublishedExceptionHandler PublishedExceptionHandler { get; set; }

		public async Task Publish<TEvent>(TEvent eventToPublish,
			bool requireUIThread = false,
			[CallerMemberName]string memberName = null,
			[CallerFilePath]string filePath = null,
			[CallerLineNumber]int lineNumber = 0)
		{
			var subscriberType = typeof(IMessageSubscriber<>).MakeGenericType(typeof(TEvent));
			var subscribers = GetSubscribersLocking(subscriberType);
			List<WeakReference> subscribersToRemove = new List<WeakReference>();

			if (requireUIThread)
			{
				var context = contextProvider.ProvidedItem;
				Debug.Assert(context != null, "contextProvider provided a null context.");

				await context.InvokeIfRequiredAsync(() => PublishCoreAsync(eventToPublish, subscribers, subscribersToRemove, true, memberName, filePath, lineNumber));
			}
			else
			{
				await PublishCoreAsync(eventToPublish, subscribers, subscribersToRemove, false, memberName, filePath, lineNumber).ConfigureAwait(false);
			}

			if (subscribersToRemove.Any())
			{
				lockSlim.EnterWriteLock();
				try
				{
					foreach (var remove in subscribersToRemove)
					{
						subscribers.Remove(remove);
					}
				}
				finally
				{
					lockSlim.ExitWriteLock();
				}
			}
		}

		async Task PublishCoreAsync<TEvent>(
			TEvent eventToPublish, 
			List<WeakReference> subscribers, 
			List<WeakReference> subscribersToRemove,
			bool continueOnCapturedContext,
			string memberName, string filePath, int lineNumber)
		{
			var exceptionHandler = PublishedExceptionHandler;

			foreach (WeakReference weakSubscriber in subscribers.ToArray())
			{
				var subscriber = (IMessageSubscriber<TEvent>)weakSubscriber.Target;
				if (subscriber != null)
				{
					if (exceptionHandler == null)
					{
						await subscriber.ReceiveMessage(eventToPublish).ConfigureAwait(continueOnCapturedContext);
					}
					else
					{
						try
						{
							await subscriber.ReceiveMessage(eventToPublish).ConfigureAwait(continueOnCapturedContext);
						}
						catch (Exception ex)
						{
							var args = new PublishedExceptionArgs(eventToPublish, subscriber, ex,
								memberName, filePath, lineNumber);
							bool handled = exceptionHandler(this, args);
							if (!handled)
							{
								throw;
							}
						}
					}
				}
				else
				{
					subscribersToRemove.Add(weakSubscriber);
				}
			}
		}

		List<WeakReference> GetSubscribersLocking(Type subscriberType)
		{
			lockSlim.EnterReadLock();
			try
			{
				return GetSubscribersNonLocking(subscriberType);
			}
			finally
			{
				lockSlim.ExitReadLock();
			}
		}

		List<WeakReference> GetSubscribersNonLocking(Type subscriberType)
		{
			List<WeakReference> subscribers;

			if (!subscriberLists.TryGetValue(subscriberType, out subscribers))
			{
				subscribers = new List<WeakReference>();
				subscriberLists.Add(subscriberType, subscribers);
			}

			return subscribers;
		}
	}

	/// <summary>
	/// This delegate is used by the default implementation of the <see cref="IMessenger"/>
	/// to allow external processing of exceptions raised during calls to publish an event.
	/// </summary>
	/// <param name="sender"></param>
	/// <param name="e">The args containing the event object, 
	/// the subscriber, and the exception raised.</param>
	/// <returns><c>true</c> if the exception has been handled 
	/// and should not be rethrown, else <c>false</c>.</returns>
	public delegate bool PublishedExceptionHandler(object sender, PublishedExceptionArgs e);

	public class PublishedExceptionArgs : EventArgs
	{
		/// <summary>
		/// The event object that is passed to the ReceiveMessage method.
		/// </summary>
		public object EventObject { get; private set; }

		/// <summary>
		/// The object that subscribed to the event, in which the exception was raised.
		/// </summary>
		public object Subscriber { get; private set; }

		/// <summary>
		/// The exception that was raised during the call to the subscriber's ReceiveMessage method.
		/// </summary>
		public Exception Exception { get; private set; }

		public string MemberName { get; private set; }
		public string FilePath { get; private set; }
		public int LineNumber { get; private set; }

		public PublishedExceptionArgs(object eventObject, object subscriber, Exception exception,
			string memberName, string filePath, int lineNumber)
		{
			EventObject = ArgumentValidator.AssertNotNull(eventObject, "eventObject");
			Subscriber = ArgumentValidator.AssertNotNull(subscriber, "subscriber");
			Exception = ArgumentValidator.AssertNotNull(exception, "exception");

			MemberName = memberName;
			FilePath = filePath;
			LineNumber = lineNumber;
		}
	}
}
